package com.panopset;

import java.io.File;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.panopset.compat.Logz;
import com.panopset.compat.Strings;
import com.panopset.io.Filez;
import com.panopset.io.LockedWriter;
import com.panopset.io.ShortFile;

/**
 * Provides list of maven dependencies from any pom, provided that the project
 * has been built. Traverses the .m2 repository for dependency hierarchy.
 * 
 * @author Karl Dinwiddie
 * 
 */
public final class MavenAuditor {

	/**
	 * Relocation xpath.
	 */
	private static final String XPATH_RELO

	= "/project/distributionManagement/relocation/groupId";

	/**
	 * Usage prompt.
	 */
	private static final String USAGE

	= "java MavenAuditor <pom.xml path> <report path>";

	/**
	 * @param args
	 *            args[1] is the path to a pom you would like a report on.
	 *            args[2] is the path to the report.
	 * @throws Exception
	 *             Exception.
	 */
	public static void main(final String... args) throws Exception {
		if (args == null || args.length != 2) {
			System.out.println(USAGE);
			return;
		}
		File pomFile = new File(args[0]);
		if (!pomFile.exists()) {
			System.out.println(new Filez().getCanonicalPath(pomFile)
					+ " does not exist.");
			return;
		}
		LockedWriter lw = new LockedWriter(args[1]);
		lw.writeln("file,group,artifact,version,scope");
		for (MavenDependency md : new MavenAuditor()
				.loadPomDependencies(pomFile)) {
			lw.writeln(md.toString());
		}
		lw.close();
	}

	/**
	 * Loaded poms.
	 */
	private List<String> loadedPoms = new ArrayList<String>();

	/**
	 * @param pomFile
	 *            Any valid pom.xml file.
	 * @return Sorted set of MavenDependency objects.
	 * @throws Exception
	 *             Exception.
	 */
	public Set<MavenDependency> loadPomDependencies(final File pomFile)
			throws Exception {
		final Set<MavenDependency> rtn = Collections
				.synchronizedSortedSet(new TreeSet<MavenDependency>());
		if (!pomFile.exists()) {
			Logz.log(new Filez().getCanonicalPath(pomFile)

			+ " does not exist.");
			return rtn;
		}
		SAXParserFactory.newInstance().newSAXParser()
				.parse(pomFile, new StandardHelper(rtn, pomFile));
		List<String> dependentPoms = new ArrayList<String>();
		for (MavenDependency md : rtn) {
			if (md.isOptional()) {
				continue;
			}
			File repositoryPom = new File(md.getLocalRepositoryPom());
			if (repositoryPom.exists() && repositoryPom.isFile()) {
				try {
					if (new ShortFile().getFileText(
							new File(md.getLocalRepositoryPom())).indexOf(
							"<relocation>") > -1) {
						md.setGroupId(new PomParser(repositoryPom)
								.getXPath(XPATH_RELO));
					}
				} catch (Exception e) {
					Logz.log(e);
				}
			}
			String fn = md.getLocalRepositoryPom();
			if (!loadedPoms.contains(fn)) {
				loadedPoms.add(fn);
				dependentPoms.add(fn);
			}
		}
		List<MavenDependency> replacements = new ArrayList<MavenDependency>();
		for (String fn : dependentPoms) {
			for (MavenDependency rmd : loadPomDependencies(new File(fn))) {
				replacements.add(rmd);
			}
		}
		for (MavenDependency repl : replacements) {
			addDependency(rtn, repl);
		}
		List<MavenDependency> dlts = new ArrayList<MavenDependency>();
		// If no version was ever specified in the pom, we go to the repository
		// and find the latest version.
		for (MavenDependency dep : rtn) {
			if (!Strings.isPopulated(dep.getVersion())) {
				StringWriter sw = new StringWriter();
				sw.append(getMavenHome());
				sw.append(sz.FILE_SEP);
				sw.append(dep.getPathFromRepositoryRoot());
				sw.append(sz.FILE_SEP);
				sw.append(dep.getArtifactId());
				String greatestVersion = "";
				File dir = new File(sw.toString());
				if (dir.exists()) {
					for (File f : dir.listFiles()) {
						if (new Strings().isNewerDotVersion(

						f.getName(), greatestVersion)) {
							greatestVersion = f.getName();
						}
					}
				}
				dep.setVersion(greatestVersion);
				if (!new File(dep.getLocalRepositoryJar()).exists()) {
					dlts.add(dep);
				}
			}
		}
		// Remove any exclusions found.
		for (MavenDependency dep : rtn) {
			for (MavenDependency exc : excs) {
				if (exc.getGroupId().equals(dep.getGroupId())
						&& exc.getArtifactId().equals(dep.getArtifactId())) {
					dlts.add(dep);
				}
			}
		}
		for (MavenDependency dep : dlts) {
			rtn.remove(dep);
		}
		return rtn;
	}

	/**
	 * Add a dependency if it is not in a <b>test</b> or <b>system</b> scope.
	 * 
	 * @param rtn
	 *            MavenDependency set.
	 * @param md
	 *            MavenDependency.
	 */
	private void addDependency(final Set<MavenDependency> rtn,
			final MavenDependency md) {

		if ("test".equals(md.getScope()) || "system".equals(md.getScope())) {
			return;
		}

		for (MavenDependency emd : rtn) {
			if (emd.getGroupId().equals(md.getGroupId())
					&& emd.getArtifactId().equals(md.getArtifactId())
					&& new Strings().isNewerDotVersion(emd.getVersion(),
							md.getVersion())) {
				return;
			}
		}
		rtn.remove(md);
		rtn.add(md);
	}

	/**
	 * Standard helper.
	 */
	class StandardHelper extends DefaultHandler {

		/**
		 * MavenDependency set.
		 */
		private final Set<MavenDependency> rtn;

		/**
		 * Pom.xml.
		 */
		private final File file;

		/**
		 * @param mavenDependencies
		 *            MavenDependency set.
		 * @param pomFile
		 *            Pom.xml.
		 */
		StandardHelper(final Set<MavenDependency> mavenDependencies,
				final File pomFile) {
			rtn = mavenDependencies;
			file = pomFile;
		}

		/**
		 * Parent project flag.
		 */
		private boolean parent = false;

		/**
		 * String buffer.
		 */
		private StringBuilder sb = new StringBuilder();

		@Override
		public void characters(final char[] buffer, final int start,
				final int length) {
			sb.append(buffer, start, length);
		}

		/**
		 * Maven dependency.
		 */
		private MavenDependency dep;

		/**
		 * Maven dependency.
		 */
		private MavenDependency exc;

		/**
		 * Project to be included as well.
		 */
		private MavenDependency project;

		@Override
		public void startElement(final String uri, final String localName,
				final String qName, final Attributes attributes) {
			sb = new StringBuilder();
			if ("dependency".equals(qName)) {
				dep = new MavenDependency(file);
			} else if ("parent".equals(qName)) {
				dep = null;
				parent = true;
			} else if ("project".equals(qName)) {
				project = new MavenDependency(file);
			} else if ("exclusion".equals(qName)) {
				exc = new MavenDependency(file);
			}
		}

		@Override
		public void endElement(final String uri, final String localName,
				final String qName) throws SAXException {
			if (qName == null) {
				return;
			}
			if ("parent".equals(qName)) {
				parent = false;
				return;
			}
			if (parent) {
				if ("groupId".equals(qName)) {
					if (exc == null) {
						if (dep == null) {
							if (project.depGroupId == null) {
								project.depGroupId = sb.toString();
							}
						} else {
							dep.setGroupId(sb.toString());
						}
					} else {
						exc.setGroupId(sb.toString());
					}
				}
				return;
			}
			if ("project".equals(qName)) {
				for (MavenDependency md : rtn) {
					md.setParent(project);
				}
				addDependency(rtn, project);
			} else if ("dependency".equals(qName)) {
				addDependency(rtn, dep);
				dep = null;
			} else if ("scope".equals(qName)) {
				if (exc == null) {
					if (dep != null) {
						dep.depScope = sb.toString();
					}
				} else {
					exc.setScope(sb.toString());
				}
			} else if ("groupId".equals(qName)) {
				if (exc == null) {
					if (dep == null) {
						if (project.depGroupId == null) {
							project.depGroupId = sb.toString();
						}
					} else {
						dep.setGroupId(sb.toString());
					}
				} else {
					exc.setGroupId(sb.toString());
				}
			} else if ("artifactId".equals(qName)) {
				if (exc == null) {
					if (dep == null) {
						if (project.depArtifactId == null) {
							project.depArtifactId = sb.toString();
						}
					} else {
						dep.setArtifactId(sb.toString());
					}
				} else {
					exc.setArtifactId(sb.toString());
				}
			} else if ("version".equals(qName)) {
				if (exc == null) {
					if (dep == null) {
						if (project.depVersion == null) {
							project.depVersion = sb.toString();
						}
					} else {
						dep.setVersion(sb.toString());
					}
				} else {
					exc.setVersion(sb.toString());
				}
			} else if ("optional".equals(qName)) {
				if (exc == null) {
					dep.optional = sb.toString();
				} else {
					exc.optional = sb.toString();
				}
			} else if ("exclusion".equals(qName)) {
				excs.add(exc);
				exc = null;
			}
		}
	}

	/**
	 * Exclusions.
	 */
	private final List<MavenDependency> excs

	= new ArrayList<MavenDependency>();

	/**
	 * Maven home.
	 */
	private static String mavenHome;

	public static void setMavenHome(final String value) {
		mavenHome = value;
	}

	/**
	 * @return System.getProperty("user.home")/.m2/repository.
	 */
	public static String getMavenHome() {
		if (mavenHome == null) {
			StringWriter sw = new StringWriter();
			sw.append(System.getProperty("user.home"));
			sw.append(sz.FILE_SEP);
			sw.append(".m2");
			sw.append(sz.FILE_SEP);
			sw.append("repository");
			mavenHome = sw.toString();
		}
		return mavenHome;
	}

	/**
	 * Maven dependency.
	 */
	public final class MavenDependency implements Comparable<MavenDependency> {

		/**
		 * Is parent set flag.
		 */
		private boolean isParentSet = false;

		/**
		 * Parent.
		 */
		private MavenDependency parent;

		/**
		 * @param parentProject
		 *            Parent project.
		 */
		public void setParent(final MavenDependency parentProject) {
			if (!isParentSet) {
				parent = parentProject;
				isParentSet = true;
			}
		}

		/**
		 * @return Parent dependency or empty String if this is top level.
		 */
		public MavenDependency getParent() {
			if (parent == null) {
				parent = this;
			}
			return parent;
		}

		/**
		 * @param pomFile
		 *            pom file.
		 */
		public MavenDependency(final File pomFile) {
			pom = pomFile;
		}

		/**
		 * @return True only if dependency is required, and not test scope.
		 */
		public boolean isRequired() {
			if (isOptional()) {
				return false;
			}
			if ("test".equalsIgnoreCase(getScope())) {
				return false;
			}
			return true;
		}

		/**
		 * Pom file this dependency was found in.
		 */
		private final File pom;

		/**
		 * @return the groupId
		 */
		public String getGroupId() {
			if (depGroupId == null) {
				depGroupId = "";
			}
			return depGroupId;
		}

		/**
		 * @param groupId
		 *            the groupId to set
		 */
		public void setGroupId(final String groupId) {
			this.depGroupId = groupId;
		}

		/**
		 * @return the artifactId
		 */
		public String getArtifactId() {
			if (depArtifactId == null) {
				depArtifactId = "";
			}
			return depArtifactId;
		}

		/**
		 * @param artifactId
		 *            the artifactId to set
		 */
		public void setArtifactId(final String artifactId) {
			this.depArtifactId = artifactId;
		}

		/**
		 * @return the version
		 */
		public String getVersion() {
			if (depVersion == null) {
				depVersion = "";
			}
			return depVersion;
		}

		/**
		 * @param version
		 *            the version to set
		 */
		public void setVersion(final String version) {
			this.depVersion = version;
		}

		/**
		 * @return the scope
		 */
		public String getScope() {
			if (depScope == null) {
				depScope = "";
			}
			return depScope;
		}

		/**
		 * @param scope
		 *            the scope to set
		 */
		public void setScope(final String scope) {
			this.depScope = scope;
		}

		/**
		 * @return pom file.
		 */
		public File getPom() {
			return pom;
		}

		/**
		 * Group id.
		 */
		private String depGroupId;

		/**
		 * Artifact id.
		 */
		private String depArtifactId;

		/**
		 * Version.
		 */
		private String depVersion;

		/**
		 * Optional.
		 */
		private String optional;

		/**
		 * Scope.
		 */
		private String depScope;

		@Override
		public int hashCode() {
			return getGroupId().hashCode() + getArtifactId().hashCode()
					+ getVersion().hashCode();
		}

		@Override
		public boolean equals(final Object obj) {
			if (obj == null) {
				return false;
			}
			if (!(obj instanceof MavenAuditor.MavenDependency)) {
				return false;
			}
			MavenAuditor.MavenDependency o = (MavenAuditor.MavenDependency) obj;
			return this.getId().equals(o.getId());
		}

		/**
		 * @return id for comparisons.
		 */
		private String getId() {
			return getArtifactId() + ";" + getGroupId() + ";" + getVersion();
		}

		/**
		 * If there is a relocation, the relocation is used, otherwise the pom
		 * group id is used to determine the path from the repository root.
		 * 
		 * @return Path from repository root.
		 */
		public String getPathFromRepositoryRoot() {
			StringWriter sw = new StringWriter();
			sw.append(getGroupId().replace(".", sz.FILE_SEP));
			return sw.toString();
		}

		/**
		 * @return Path to pom in local repository.
		 */
		public String getRepository() {
			StringWriter sw = new StringWriter();
			sw.append(getPathFromRepositoryRoot());
			sw.append(sz.FILE_SEP);
			sw.append(getArtifactId());
			sw.append(sz.FILE_SEP);
			sw.append(getVersion());
			return sw.toString();
		}

		/**
		 * @return True if dependency is marked optional in the pom.
		 */
		public boolean isOptional() {
			if (Strings.isPopulated(optional)
					&& "true".equalsIgnoreCase(optional)) {
				return true;
			}
			return false;
		}

		/**
		 * @return name.
		 */
		public String getName() {
			StringWriter sw = new StringWriter();
			sw.append(getArtifactId());
			sw.append("-");
			sw.append(getVersion());
			return sw.toString();
		}

		/**
		 * @return Relative path to pom.
		 */
		public String getRelativeRepositoryPom() {
			StringWriter sw = new StringWriter();
			sw.append(getRepository());
			sw.append(sz.FILE_SEP);
			sw.append(getPomName());
			return sw.toString();
		}

		/**
		 * @return Relative path to jar.
		 */
		public String getRelativeRepositoryJar() {
			StringWriter sw = new StringWriter();
			sw.append(getRepository());
			sw.append(sz.FILE_SEP);
			sw.append(getJarName());
			return sw.toString();
		}

		/**
		 * @return Path to pom in local repository.
		 */
		public String getLocalRepositoryPom() {
			StringWriter sw = new StringWriter();
			sw.append(getMavenHome());
			sw.append(sz.FILE_SEP);
			sw.append(getRelativeRepositoryPom());
			return sw.toString();
		}

		/**
		 * @return Path to jar in local repository.
		 */
		public String getLocalRepositoryJar() {
			StringWriter sw = new StringWriter();
			sw.append(getMavenHome());
			sw.append(sz.FILE_SEP);
			sw.append(getRelativeRepositoryJar());
			return sw.toString();
		}

		/**
		 * @return Simple jar name.
		 */
		public String getJarName() {
			StringWriter sw = new StringWriter();
			sw.append(getName());
			sw.append(".jar");
			return sw.toString();
		}

		/**
		 * @return Simple pom name.
		 */
		public String getPomName() {
			StringWriter sw = new StringWriter();
			sw.append(getName());
			sw.append(".pom");
			return sw.toString();
		}

		@Override
		public String toString() {
			StringWriter sw = new StringWriter();
			sw.append(fz.getCanonicalPath(pom));
			sw.append(",");
			sw.append(getGroupId());
			sw.append(",");
			sw.append(getArtifactId());
			sw.append(",");
			sw.append(getVersion());
			sw.append(",");
			sw.append(getScope());
			return sw.toString();
		}

		@Override
		public int compareTo(final MavenDependency o) {
			return getId().compareTo(o.getId());
		}
	}

	private static final Filez fz = new Filez();
	private static final Strings sz = new Strings();
}
